using System.Collections.Generic;
using System;
using Cysharp.Threading.Tasks;
using EarthGeospatial;
using EarthUtility;
using Geometry;
using Gltf;
using Unity.Mathematics;

namespace Cesium3DTilesSelection
{
    internal class GltfContent : TileContentLoader
    {
        public override UniTask<TileContentLoadResult> load(TileContentLoadInput input)
        {
            throw new System.NotImplementedException();
        }

        internal static BoundingRegion createRasterOverlayTextureCoordinates(Model gltf, double4x4 transform, int textureCoordinateID,
        Projection projection, Rectangle rectangle)
        {
            int[] positionAccessorsToTextureCoordinateAccessor = new int[gltf.accessors.Count];
            string attributeName = "_CESIUMOVERLAY_" + textureCoordinateID;
            double west = Mathd.ONE_PI;
            double south = Mathd.PI_OVER_TWO;
            double east = -Mathd.ONE_PI;
            double north = -Mathd.PI_OVER_TWO;
            double minimumHeight = double.MaxValue;
            double maximumHeight = double.MinValue;
            gltf.forEachPrimitiveInScene(-1,
            (Model gltf_, Node node, Mesh mesh, MeshPrimitive primitive, double4x4 nodeTransform) =>
            {
                var positionIt = primitive.attributes.TryGetValue("POSITION", out int positionAccessorIndex);
                if (positionIt == false)
                {
                    return;
                }
                if (positionAccessorIndex < 0 ||
                    positionAccessorIndex >= gltf_.accessors.Count)
                {
                    return;
                }

                int textureCoordinateAccessorIndex = positionAccessorsToTextureCoordinateAccessor[(positionAccessorIndex)];
                if (textureCoordinateAccessorIndex > 0)
                {
                    primitive.attributes[attributeName] = textureCoordinateAccessorIndex;
                    return;
                }

                // TODO remove this check
                if (primitive.attributes.ContainsKey(attributeName))
                {
                    return;
                }

                double4x4 fullTransform = math.mul(transform, math.mul(AxisTransforms.Y_UP_TO_Z_UP, nodeTransform));

                // Generate new texture coordinates
                int nextTextureCoordinateAccessorIndex = generateOverlayTextureCoordinates(
                       gltf_,
                       positionAccessorIndex,
                       fullTransform,
                       projection,
                       rectangle,
                       west,
                       south,
                       east,
                       north,
                       minimumHeight,
                       maximumHeight);
                if (nextTextureCoordinateAccessorIndex < 0)
                {
                    return;
                }

                primitive.attributes[attributeName] = nextTextureCoordinateAccessorIndex;
                positionAccessorsToTextureCoordinateAccessor[(positionAccessorIndex)] = nextTextureCoordinateAccessorIndex;
            });
            return new BoundingRegion(new GlobeRectangle(west, south, east, north), minimumHeight, maximumHeight);
        }

        private static int generateOverlayTextureCoordinates(Model gltf, int positionAccessorIndex, double4x4 transform, Projection projection, Rectangle rectangle, double west, double south, double east, double north, double minimumHeight, double maximumHeight)
        {
            List<Gltf.Buffer> buffers = gltf.buffers;
            List<Gltf.BufferView> bufferViews = gltf.bufferViews;
            List<Gltf.Accessor> accessors = gltf.accessors;

            int uvBufferId = (buffers.Count);
            Gltf.Buffer uvBuffer = new Gltf.Buffer();
            buffers.Add(uvBuffer);

            int uvBufferViewId = (bufferViews.Count);
            bufferViews.Add(new BufferView());


            int uvAccessorId = (accessors.Count);
            accessors.Add(new Accessor());

            AccessorView<float3> positionView = new AccessorView<float3>(gltf, positionAccessorIndex);
            if (positionView.status() != AccessorViewStatus.Valid)
            {
                return -1;
            }

            uvBuffer.cesium.data = new byte[(positionView.size()) * 2 * sizeof(float)];

            BufferView uvBufferView = gltf.bufferViews[(uvBufferViewId)];
            uvBufferView.buffer = uvBufferId;
            uvBufferView.byteOffset = 0;
            uvBufferView.byteStride = 2 * sizeof(float);
            uvBufferView.byteLength = (Int64)(uvBuffer.cesium.data.Length);
            uvBufferView.target = BufferView.Target.ARRAY_BUFFER;

            Accessor uvAccessor = gltf.accessors[(uvAccessorId)];
            uvAccessor.bufferView = uvBufferViewId;
            uvAccessor.byteOffset = 0;
            uvAccessor.componentType = Accessor.ComponentType.FLOAT;
            uvAccessor.count = (Int64)(positionView.size());
            uvAccessor.type = Accessor.Type.VEC2;

            AccessorWriter<float2> uvWriter = new AccessorWriter<float2>(gltf, uvAccessorId);
            // assert(uvWriter.status() == CesiumGltf::AccessorViewStatus::Valid);

            double width = rectangle.computeWidth();
            double height = rectangle.computeHeight();

            for (int i = 0; i < positionView.size(); ++i)
            {
                // float[] farr = new float[positionView._pData.Length / 4];
                // System.Buffer.BlockCopy(positionView._pData, 0, farr, 0, positionView._pData.Length);
                // Get the ECEF position
                float3 position = positionView[i];
                double3 positionEcef = math.mul(transform, new double4(position, 1.0)).xyz;

                // Convert it to cartographic
                Cartographic? cartographic = Ellipsoid.WGS84.cartesianToCartographic(positionEcef);
                if (!cartographic.HasValue)
                {
                    uvWriter[i] = new float2(0.0f, 0.0f);
                    continue;
                }

                // Project it with the raster overlay's projection
                double3 projectedPosition = Projection.projectPosition(projection, cartographic.Value);

                double longitude = cartographic.Value.longitude;
                double latitude = cartographic.Value.latitude;
                double ellipsoidHeight = cartographic.Value.height;

                // If the position is near the anti-meridian and the projected position is
                // outside the expected range, try using the equivalent longitude on the
                // other side of the anti-meridian to see if that gets us closer.
                if (math.abs(math.abs(cartographic.Value.longitude) - Mathd.ONE_PI) < Mathd.EPSILON5 &&
                    (projectedPosition.x < rectangle.minimumX ||
                     projectedPosition.x > rectangle.maximumX ||
                     projectedPosition.y < rectangle.minimumY ||
                     projectedPosition.y > rectangle.maximumY))
                {
                    var cartographicReal = cartographic.Value;
                    cartographicReal.longitude += cartographic.Value.longitude < 0.0 ? Mathd.TWO_PI : -Mathd.TWO_PI;
                    cartographic = cartographicReal;
                    double3 projectedPosition2 = Projection.projectPosition(projection, cartographic.Value);

                    double distance1 = rectangle.computeSignedDistance(projectedPosition.xy);
                    double distance2 = rectangle.computeSignedDistance(projectedPosition2.xy);

                    if (distance2 < distance1)
                    {
                        projectedPosition = projectedPosition2;
                        longitude = cartographic.Value.longitude;
                    }
                }

                // The computation of longitude is very unstable at the poles,
                // so don't let extreme latitudes affect the longitude bounding box.
                if (math.abs(math.abs(latitude) - Mathd.PI_OVER_TWO) > Mathd.EPSILON6)
                {
                    west = math.min(west, longitude);
                    east = math.max(east, longitude);
                }
                south = math.min(south, latitude);
                north = math.max(north, latitude);
                minimumHeight = math.min(minimumHeight, ellipsoidHeight);
                maximumHeight = math.max(maximumHeight, ellipsoidHeight);

                // Scale to (0.0, 0.0) at the (minimumX, minimumY) corner, and (1.0, 1.0) at
                // the (maximumX, maximumY) corner. The coordinates should stay inside these
                // bounds if the input rectangle actually bounds the vertices, but we'll
                // clamp to be safe.
                float2 uv = new float2((float)math.clamp((projectedPosition.x - rectangle.minimumX) / width, 0.0, 1.0), (float)math.clamp((projectedPosition.y - rectangle.minimumY) / height, 0.0, 1.0));
                uvWriter[i] = uv;
            }

            return uvAccessorId;
        }
    }
}